﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Reflection;

namespace MemoDb
{
    public class MemoSession : IDisposable
    {
        private readonly IDictionary<Type, TypeStorage> storages;
        private readonly Memo memo;
        private readonly Queue objectsToInsert = new Queue();
        private readonly Queue objectsToDelete = new Queue();
        private readonly Queue objectsToUpdate = new Queue();
        private readonly ObjectsCache objectsCache = new ObjectsCache();

        public MemoSession(IDictionary<Type, TypeStorage> storages, Memo memo)
        {
            this.storages = storages;
            this.memo = memo;
        }

        public T GetById<T>(object id) where T : class
        {
            var obj = StorageFor(typeof(T)).GetById(id);

            var clone = GetForThisSession(obj, cloneSource: true);

            return (T)clone;
        }

        private TypeStorage StorageFor(Type type)
        {
            if (!storages.ContainsKey(type))
                throw new InvalidOperationException(string.Format("Type {0} was not mapped.", type));

            return storages[type];
        }

        public void Insert(object obj)
        {
            objectsToInsert.Enqueue(obj);

            if (memo.AssignIdMode == AssignIdMode.OnInsert)
                StorageFor(obj.GetType()).IdMapper.SetNewIdIfNeeded(obj);
        }

        public void Delete<T>(T source)
        {
            objectsToDelete.Enqueue(source);
        }

        public void Flush()
        {
            ValidateRelationshipWereProperlySaved();
            FlushInserts();
            FlushUpdates();
            FlushDeletes();
        }

        private void ValidateRelationshipWereProperlySaved()
        {
            var allRoots = objectsToInsert.Cast<object>().Union(objectsCache.GetAll()).ToList();

            var referencesOnCollections =
                allRoots.SelectMany(obj => Properties(obj.GetType()).Where(p => IsCollection(p) && !CascadeInsert(p)),
                                    (obj, property) => (IEnumerable)property.GetValue(obj, null)).Where(x => x != null).SelectMany(x => x.Cast<object>())
                    .Where(x => x != null && IsMapped(x.GetType()));

            var dictionaries =
                allRoots.SelectMany(obj => Properties(obj.GetType()).Where(p => IsDictionary(p) && !CascadeInsert(p)),
                                    (obj, property) => (IDictionary)property.GetValue(obj, null)).Where(x => x != null);

            var referencesOnDictionaryKeys = dictionaries.SelectMany(x => x.Keys.Cast<object>());
            var referencesOnDictionaryValues = dictionaries.SelectMany(x => x.Values.Cast<object>());

            var references =
                allRoots.SelectMany(x => Properties(x.GetType()).Where(p => !CascadeInsert(p)),
                    (x, property) => property.GetValue(x, null));


            var allReferencesToValidate = references.Union(referencesOnCollections)
                .Union(referencesOnDictionaryKeys)
                .Union(referencesOnDictionaryValues).Where(x => x != null && IsMapped(x.GetType()));

            foreach (var reference in allReferencesToValidate)
            {
                if (!allRoots.Contains(reference))
                    throw new MemoDbException(string.Format("Unsaved object of type {0}", reference.GetType().FullName));
            }
        }

        private void FlushDeletes()
        {
            while (objectsToDelete.Count > 0)
            {
                var obj = objectsToDelete.Dequeue();
                var type = obj.GetType();

                var references =
                    Properties(type).Where(x => IsCollection(x) && memo.CascadeDeleteOrphans(x)).Select(
                        x => (IEnumerable)x.GetValue(obj, null));

                var dictionaries =
                    Properties(type).Where(x => IsDictionary(x) && memo.CascadeDeleteOrphans(x)).Select(
                        x => (IDictionary)x.GetValue(obj, null));
                
                var orphans = references.SelectMany(x => x.Cast<object>())
                    .Union(dictionaries.SelectMany(x => x.Keys.Cast<object>()))
                    .Union(dictionaries.SelectMany(x => x.Values.Cast<object>()))
                    .Where(x => IsMapped(x.GetType()));

                foreach (var orphan in orphans)
                    Delete(orphan);

                var storage = StorageFor(obj.GetType());
                storage.Delete(obj);
                objectsCache.Remove(storage.Type, storage.IdMapper.GetId(obj));
            }
        }
        
        
        private void FlushUpdates()
        {
            foreach (var obj in objectsCache.GetAll().Where(obj => !objectsToUpdate.Contains(obj)))
                objectsToUpdate.Enqueue(obj);

            while (objectsToUpdate.Count > 0)
            {
                var obj = objectsToUpdate.Dequeue();
                var type = obj.GetType();
                UpdateReferencesWithThisSession(obj);

                var storage = StorageFor(type);

                storage.IdMapper.SetNewIdIfNeeded(obj);

                storage.Update(Clone(obj));

                var id = storage.IdMapper.GetId(obj);
                if(objectsCache.Get(type, id) == null )
                    objectsCache.Add(type, id, obj);
            }
        }

        private static IEnumerable<PropertyInfo> Properties(Type type)
        {
            return type.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).Where(x => x.CanWrite);
        }

        private void UpdateReferencesWithThisSession(object obj)
        {
            foreach (var property in Properties(obj.GetType()))
            {
                var value = GetPropertyValue(property, obj);
                SetProperty(property, value, to: obj);
            }
        }

        private void FlushInserts()
        {
            while (objectsToInsert.Count > 0)
            {
                var obj = objectsToInsert.Dequeue();
                UpdateReferencesWithThisSession(obj);
                Attach(obj); //it will be saved when flushing the updates
            }
        }

        private object GetForThisSession(object source, bool cloneSource)
        {
            if (source == null)
                return null;

            var type = source.GetType();
            var id = StorageFor(type).IdMapper.GetId(source);

            var obj = objectsCache.Get(type, id);

            if (obj != null)
                return obj;

            if (cloneSource)
            {
                obj = Clone(source, attach: true);
            }
            else
            {
                obj = source;
                Attach(obj);
            }

            return obj;
        }

        private void Attach(object obj)
        {
            var type = obj.GetType();
            if (!StorageFor(type).IsNew(obj))
            {
                var id = StorageFor(type).IdMapper.GetId(obj);
                objectsCache.Add(type, id, obj);
            }
            objectsToUpdate.Enqueue(obj);
        }

        private object Clone(object source, bool attach=false)
        {
            var type = source.GetType();
            var clone = Activator.CreateInstance(type);
          
            var idMapper = StorageFor(type).IdMapper;
            idMapper.Property.SetValue(clone, idMapper.GetId(source), null);

            if (attach)
                Attach(clone);
            
            foreach (var property in Properties(type).Where(x=>x != idMapper.Property))
            {
                var value = GetPropertyValue(property, source);
                SetProperty(property, value, to: clone);
            }
            
            return clone;
        }

        private static void SetProperty(PropertyInfo property, object value, object to)
        {
            if (IsEntityRef(property.PropertyType))
            {
                var entityType = property.PropertyType.GetGenericArguments().Single();
                value = property.PropertyType.GetConstructor(new[] { entityType }).Invoke(new[] { value });
            }
            property.SetValue(to, value, null);
        }

        private object GetPropertyValue(PropertyInfo property, object source)
        {
            var value = property.GetValue(source, null);

            if (IsDictionary(property))
            {
                var builder = new DictionaryBuilder();
                var dictionary = (IDictionary)value;

                if (value != null)
                    foreach (var key in dictionary.Keys)
                        builder.Add(PrepareValue(key), PrepareValue(dictionary[key]));

                return builder.BuildFor(property.PropertyType);

            }
            if (IsCollection(property))
            {
                var builder = new CollectionBuilder();

                if (value != null)
                    foreach (var item in (IEnumerable)value)
                        builder.Add(PrepareValue(item));

                return builder.BuildFor(property.PropertyType);
            }
            return PrepareValue(value);
        }

        private static bool IsDictionary(PropertyInfo property)
        {
            var type = property.PropertyType;

            if (typeof(IDictionary).IsAssignableFrom(type))
                return true;

            return type.GetInterfaces().Union(new[] { type }).Any(
                    x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>));
        }

        private static bool IsCollection(PropertyInfo property)
        {
            return typeof(IEnumerable).IsAssignableFrom(property.PropertyType) && property.PropertyType != typeof(string) && !IsDictionary(property);
        }

        private object PrepareValue(object source)
        {
            if (source == null)
                return null;

            Type type;
            var value = source;
            if (IsEntityRef(source.GetType()))
            {
                type = source.GetType().GetGenericArguments().Single();
                var property = source.GetType().GetProperty("Entity");
                value = property.GetValue(source, null);
            }
            else
            {
                type = source.GetType();
            }

            if (IsMapped(type))
            {
                var storage = storages[type];
                var id = storage.IdMapper.GetId(value);
                var cloneSource = storage.Contains(id);
                value = GetForThisSession(value, cloneSource);
            }
            return value;
        }

        private static bool IsEntityRef(Type type)
        {
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(EntityRef<>);
        }

        private bool CascadeInsert(PropertyInfo property)
        {
            var e = memo.CascadeInsert(property);
            return e;
        }

        private bool IsMapped(Type type)
        {
            return storages.ContainsKey(type);
        }

        public void Dispose()
        {
            Clear();
        }

        public IQueryable<T> Query<T>()
        {
            var storage = StorageFor(typeof(T));
            return new MemoQuery<T>(() =>
                                    storage.Items.Cast<T>()
                                        .Select(x => (T)GetForThisSession(x, cloneSource: true))
                                        .Where(x => !objectsToDelete.Contains(x))
                                        .Union(objectsToInsert.OfType<T>())
                );
        }

        public void Clear()
        {
            objectsToInsert.Clear();
            objectsToDelete.Clear();
            objectsCache.Clear();
        }
    }
}